const String = @import("ztring").String;
const std = @import("std");
const db = @import("./services/db.zig");
const myzql = @import("myzql");
const ResultSet = myzql.result.ResultSet;
const ResultRow = myzql.result.ResultRow;
const BinaryResultRow = myzql.result.BinaryResultRow;

const COVER_EXIST_FILES: bool = true;

const TableColumnInfo = struct {
    column_name: ?[]const u8 = null,
    data_type: ?[]const u8 = null,
    character_maximum_length: ?i64 = null,
    is_nullable: ?[]const u8 = null,
    column_default: ?i64 = null,
    extra: ?[]const u8 = null,
    pk: bool,
};

const GenLineRet = struct { field: String, have_time_field: bool };

fn gen_model_struct_field(table_column_info: *const TableColumnInfo) !GenLineRet {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    var line = try String.init_with_contents(allocator, "\t");
    try line.concat(table_column_info.column_name.?);
    try line.concat(":");
    var have_time_field = false;

    if (string_contains(table_column_info.data_type.?, "char") or
        string_contains(table_column_info.data_type.?, "text") or
        string_contains(table_column_info.data_type.?, "blob") or
        string_contains(table_column_info.data_type.?, "binary") or
        string_contains(table_column_info.data_type.?, "enum") or
        string_contains(table_column_info.data_type.?, "set"))
    {
        try line.concat(" ?[]const u8 = null");
    } else if (string_contains(table_column_info.data_type.?, "int") or string_contains(table_column_info.data_type.?, "bigint")) {
        try line.concat(" ?i32 = null");
    } else if (string_contains(table_column_info.data_type.?, "bigint")) {
        try line.concat(" ?i64 = null");
    } else if (string_contains(table_column_info.data_type.?, "date") or
        string_contains(table_column_info.data_type.?, "time") or
        string_contains(table_column_info.data_type.?, "datetime") or
        string_contains(table_column_info.data_type.?, "timestamp") or
        string_contains(table_column_info.data_type.?, "year"))
    {
        try line.concat(" ?DateTime = null");
        have_time_field = true;
    }
    try line.concat(",");
    return GenLineRet{ .field = line, .have_time_field = have_time_field };
}

fn gen_service_concat_field(table_column_info: *const TableColumnInfo) !GenLineRet {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    var item_content = try String.init_with_contents(allocator, CONCAT_FUNC_ITEM);
    _ = try item_content.replace("{%field_name%}", table_column_info.column_name.?);

    return GenLineRet{ .field = item_content, .have_time_field = false };
}

fn model_concat_cols_make_line(table_column_info: *const TableColumnInfo) !String {
    var line = try String.init_with_contents(std.heap.page_allocator, "\t\t\t\tparam.");
    try line.concat("\t\tif param.");
    try line.concat(table_column_info.column_name.?);
    try line.concat(".is_some() {\n");

    try line.concat("\t\t\ts += &*(conj.to_owned() + \"");
    try line.concat(table_column_info.column_name.?);
    try line.concat(" = ?\");\n");

    try line.concat("\t\t\tv.push(param.");
    try line.concat(table_column_info.column_name.?);
    try line.concat(".clone().unwrap().to_string());\n");

    try line.concat("\t\t}\n");

    return line;
}

fn model_insert_line(table_column_info: *const TableColumnInfo) !String {
    var line = try String.init_with_contents(std.heap.page_allocator, "\t\t\t\tparam.");
    try line.concat(table_column_info.column_name.?);
    try line.concat(".clone().unwrap().to_string(),\n");
    return line;
}

fn model_query_line(table_column_info: *const TableColumnInfo) !String {
    var line = try String.init_with_contents(std.heap.page_allocator, "\t\t\t\t\t\t");
    try line.concat(table_column_info.column_name.?);
    try line.concat(": Some(");
    try line.concat(table_column_info.column_name.?);
    try line.concat("),\n");
    return line;
}

fn gen_model_file(table_name: String, table_column_infos: []TableColumnInfo) !void {
    var fields = String.init(std.heap.page_allocator);
    var have_time_field = false;
    var insert_lines = String.init(std.heap.page_allocator);
    var filed_names = String.init(std.heap.page_allocator);
    var prep_names = String.init(std.heap.page_allocator);
    var query_lines = String.init(std.heap.page_allocator);
    var model_concat_cols_make_lines = String.init(std.heap.page_allocator);

    for (table_column_infos) |table_column_info| {
        const genLineRet = try gen_model_struct_field(&table_column_info);
        if (genLineRet.have_time_field) {
            have_time_field = true;
        }
        try fields.concat("\n");
        try fields.concat(genLineRet.field.str());

        const insert_line = try model_insert_line(&table_column_info);
        try insert_lines.concat(insert_line.str());

        try filed_names.concat(table_column_info.column_name.?);
        try filed_names.concat(",");

        try prep_names.concat("?,");

        const query_line = try model_query_line(&table_column_info);
        try query_lines.concat(query_line.str());
        const model_concat_cols_line = try model_concat_cols_make_line(&table_column_info);
        try model_concat_cols_make_lines.concat(model_concat_cols_line.str());
    }
    filed_names.trimEnd(",");
    prep_names.trimEnd("?,");

    const table_name_lower = table_name;
    const table_name_capital = try create_capitalize_string(&table_name);

    var use_library = String.init(std.heap.page_allocator);
    if (have_time_field) {
        try use_library.concat(
            \\const DateTime = @import("myzql").temporal.DateTime;
        );
    }

    var file_content = try String.init_with_contents(std.heap.page_allocator, MODEL_TEMPLATE);
    _ = try file_content.replace("{%table_name_capital%}", table_name_capital.str());
    _ = try file_content.replace("{%table_name_lower%}", table_name_lower.str());
    _ = try file_content.replace("{%fields%}", fields.str());
    _ = try file_content.replace("{%use_library%}", use_library.str());
    _ = try file_content.replace("{%insert_lines%}", insert_lines.str());
    _ = try file_content.replace("{%filed_names%}", filed_names.str());
    _ = try file_content.replace("{%prep_names%}", prep_names.str());
    _ = try file_content.replace("{%query_lines%}", query_lines.str());
    _ = try file_content.replace(
        "{%model_concat_cols_make_lines%}",
        model_concat_cols_make_lines.str(),
    );

    var filename = try String.init_with_contents(std.heap.page_allocator, table_name_lower.str());
    try filename.concat("_model.zig");
    const dir = try String.init_with_contents(std.heap.page_allocator, "src/models/");
    try gen_file(dir, filename, file_content);
}

fn gen_service_file(table_name: String, pk_name: []const u8, table_column_infos: []TableColumnInfo) !void {
    const table_name_lower = table_name;
    var table_name_capital = try create_capitalize_string(&table_name);
    var concat_items = String.init(std.heap.page_allocator);
    var insert_fields = String.init(std.heap.page_allocator);

    for (table_column_infos) |table_column_info| {
        const genLineRet = try gen_service_concat_field(&table_column_info);
        try concat_items.concat(genLineRet.field.str());
        try insert_fields.concat(table_name.str());
        try insert_fields.concat(".");
        try insert_fields.concat(table_column_info.column_name.?);
        try insert_fields.concat(".?, ");
    }

    var file_content = try String.init_with_contents(std.heap.page_allocator, SERVICE_TEMPLATE);
    _ = try file_content.replace("{%table_name_capital%}", table_name_capital.str());
    _ = try file_content.replace("{%table_name_lower%}", table_name_lower.str());
    _ = try file_content.replace("{%concat_items%}", concat_items.str());
    _ = try file_content.replace("{%insert_fields%}", insert_fields.str());
    _ = try file_content.replace("{%pk_name%}", pk_name);

    var filename = try String.init_with_contents(std.heap.page_allocator, table_name_lower.str());
    try filename.concat("_service.zig");
    const dir = try String.init_with_contents(std.heap.page_allocator, "src/services/");
    try gen_file(dir, filename, file_content);
}

fn gen_controller_file(table_name: *const String, pk_name: []const u8) !void {
    const table_name_lower = table_name;
    const table_name_capital = try create_capitalize_string(table_name);

    var file_content = try String.init_with_contents(std.heap.page_allocator, CONTROLLER_TEMPLATE);

    _ = try file_content.replace("{%table_name_capital%}", table_name_capital.str());
    _ = try file_content.replace("{%table_name_lower%}", table_name_lower.str());
    _ = try file_content.replace("{%pk_name%}", pk_name);

    var filename = try String.init_with_contents(std.heap.page_allocator, table_name_lower.str());
    try filename.concat("_controller.zig");
    const dir = try String.init_with_contents(std.heap.page_allocator, "src/controllers/");
    try gen_file(dir, filename, file_content);
}

const QueryColInfo = struct {
    column_name: ?[]const u8 = null,
    data_type: ?[]const u8 = null,
    character_maximum_length: ?i64 = null,
    is_nullable: ?[]const u8 = null,
    column_default: ?i64 = null,
    extra: ?[]const u8,
};

const PkNameWrap = struct {
    pk_name: ?[]const u8 = null,
};

fn gen_table_fields(dbname: String, tables: []String) !void {
    const client = db.get_db_client();
    for (tables) |table| {
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};
        const allocator = gpa.allocator();

        const dn = (try dbname.toOwned()).?;
        const tb = (try table.toOwned()).?;

        const pk_sql =
            \\SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
            \\WHERE TABLE_SCHEMA=? AND TABLE_NAME=? AND COLUMN_KEY='PRI'
        ;
        const pk_prep_res = try client.prepare(allocator, pk_sql);
        defer pk_prep_res.deinit(allocator);
        const pk_prep_stmt = try pk_prep_res.expect(.stmt);
        const pk_query_res = try client.executeRows(&pk_prep_stmt, .{
            dn,
            tb,
        });
        const pk_rows: ResultSet(BinaryResultRow) = try pk_query_res.expect(.rows);
        const pk_names = try pk_rows.iter().tableStructs(PkNameWrap, allocator);
        defer pk_names.deinit(allocator);
        const pk_name = pk_names.struct_list.items[0].pk_name.?;

        const col_sql =
            \\SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE, COLUMN_DEFAULT,EXTRA
            \\FROM information_schema.COLUMNS
            \\WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
        ;

        const col_prep_res = try client.prepare(allocator, col_sql);
        defer col_prep_res.deinit(allocator);
        const col_prep_stmt = try col_prep_res.expect(.stmt);
        // const col_query_res = try client.executeRows(&col_prep_stmt, .{ dbname.str(), table.str() });

        const col_query_res = try client.executeRows(&col_prep_stmt, .{
            dn,
            tb,
        });
        const col_rows: ResultSet(BinaryResultRow) = try col_query_res.expect(.rows);

        var info_arr = std.ArrayList(TableColumnInfo).init(allocator);
        defer info_arr.deinit();
        while (try col_rows.iter().next()) |row| {
            var queryColInfo: QueryColInfo = undefined;
            try row.scan(&queryColInfo);
            const is_pk = std.mem.eql(u8, pk_name, queryColInfo.column_name.?);
            const info = TableColumnInfo{ .column_name = queryColInfo.column_name, .data_type = queryColInfo.data_type, .character_maximum_length = queryColInfo.character_maximum_length, .extra = queryColInfo.extra, .column_default = queryColInfo.column_default, .is_nullable = queryColInfo.is_nullable, .pk = is_pk };
            try info_arr.append(info);
        }

        try gen_model_file(table, info_arr.items);
        try gen_service_file(table, pk_name, info_arr.items);
        try gen_controller_file(&table, pk_name);
    }
}

fn check_files_exist(tables: []String) !bool {
    const dirs = [][]u8{
        "src/controllers/",
        "src/models/",
        "src/services/",
        "src/routers/",
    };
    var exist_files = std.ArrayList(String).init(std.heap.page_allocator);
    defer exist_files.deinit();
    for (dirs) |dir| {
        for (tables) |table| {
            var pathname = try String.init_with_contents(std.heap.page_allocator, dir);
            try pathname.concat(table.str());
            try pathname.concat(".zig");
            if (std.fs.cwd().access(pathname.str(), .{})) |_| {
                exist_files.append(table);
            }
        }
    }

    if (exist_files.items.len > 0) {
        std.debug.print("待生成MVC文件已有存在, 请先移除: ", .{});
        for (exist_files) |file| {
            std.debug.print("\t{s}", .{file.str()});
        }
        return true;
    }

    return false;
}

const ToCreatedDbTables = struct { db_name: String, tables: []String };

fn connect_db_and_show_tables() !ToCreatedDbTables {
    const db_name = try String.init_with_contents(std.heap.page_allocator, "default");
    const t = try String.init_with_contents(std.heap.page_allocator, "teacher");
    var arr = std.ArrayList(String).init(std.heap.page_allocator);
    try arr.append(t);
    return ToCreatedDbTables{ .db_name = db_name, .tables = arr.items };
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    try db.init(allocator);
    const toCreatedDbTables = try connect_db_and_show_tables();
    if (toCreatedDbTables.tables.len < 1) {
        std.debug.print("Done.Empty table list.", .{});
        return;
    }

    if (!COVER_EXIST_FILES) {
        const exist = try check_files_exist(toCreatedDbTables.tables);
        if (exist) {
            return;
        }
    }

    try gen_table_fields(toCreatedDbTables.db_name, toCreatedDbTables.tables);
    std.debug.print("Done.Success.", .{});
}

fn gen_file(dir: String, filename: String, content: String) !void {
    var output_dir = try std.fs.cwd().openDir(dir.str(), .{});
    const file = try output_dir.createFile(filename.str(), .{});
    try file.writeAll(content.str());
}

fn string_contains(s: []const u8, sub: []const u8) bool {
    _ = std.mem.indexOf(u8, s, sub) orelse {
        return false;
    };

    return true;
}

fn create_capitalize_string(s: *const String) !String {
    var c = try String.init_with_contents(std.heap.page_allocator, s.str());
    c.toCapitalized();
    return c;
}

const MODEL_TEMPLATE =
    \\{%use_library%}
    \\
    \\pub const {%table_name_capital%} = struct {{%fields%}
    \\};
;

const SERVICE_TEMPLATE =
    \\const std = @import("std");
    \\const db = @import("./db.zig");
    \\const myzql = @import("myzql");
    \\const String = @import("ztring").String;
    \\const ResultSet = myzql.result.ResultSet;
    \\const ResultRow = myzql.result.ResultRow;
    \\const TextResultRow = myzql.result.TextResultRow;
    \\const ResultRowIter = myzql.result.ResultRowIter;
    \\const TextElemIter = myzql.result.TextElemIter;
    \\const BinaryResultRow = myzql.result.BinaryResultRow;
    \\const httpz = @import("httpz");
    \\const {%table_name_lower%}_model = @import("../models/{%table_name_lower%}_model.zig");
    \\
    \\pub fn add({%table_name_lower%}: *const {%table_name_lower%}_model.{%table_name_capital%}) !bool {
    \\    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    \\    const allocator = gpa.allocator();
    \\    const client = db.get_db_client();
    \\    const prep_res = try client.prepare(
    \\        allocator,
    \\        "INSERT INTO {%table_name_lower%}(name, number) VALUES (?, ?)",
    \\    );
    \\    defer prep_res.deinit(allocator);
    \\    const prep_stmt = try prep_res.expect(.stmt);
    \\    const param = .{ {%insert_fields%} };
    \\    _ = try client.execute(&prep_stmt, param);
    \\    return true;
    \\}
    \\
    \\pub fn query({%table_name_lower%}: *const {%table_name_lower%}_model.{%table_name_capital%}, response: *httpz.Response) !void {
    \\    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    \\    const allocator = gpa.allocator();
    \\    var sql = String.init(allocator);
    \\    defer sql.deinit();
    \\    try sql.concat("SELECT * FROM {%table_name_lower%} ");
    \\    try concat_cols_push(&sql, "WHERE ", {%table_name_lower%}, "AND ");
    \\    const client = db.get_db_client();
    \\    const prep_res = try client.prepare(allocator, sql.str());
    \\    defer prep_res.deinit(allocator);
    \\    const prep_stmt = try prep_res.expect(.stmt);
    \\    const query_res = try client.executeRows(&prep_stmt, .{});
    \\    const rows: ResultSet(BinaryResultRow) = try query_res.expect(.rows);
    \\    const res_{%table_name_lower%}s = try rows.iter().tableStructs({%table_name_lower%}_model.{%table_name_capital%}, allocator);
    \\    defer res_{%table_name_lower%}s.deinit(allocator);
    \\    try response.json(.{ .result = res_{%table_name_lower%}s.struct_list.items }, .{});
    \\}
    \\
    \\pub fn query_by_pk({%pk_name%}: i32) !{%table_name_lower%}_model.{%table_name_capital%} {
    \\    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    \\    const allocator = gpa.allocator();
    \\    const client = db.get_db_client();
    \\    const prep_res = try client.prepare(allocator, "SELECT * from {%table_name_lower%} WHERE {%pk_name%} = ?");
    \\    defer prep_res.deinit(allocator);
    \\    const prep_stmt = try prep_res.expect(.stmt);
    \\    const query_res = try client.executeRows(&prep_stmt, .{{%pk_name%}});
    \\    const rows: ResultSet(BinaryResultRow) = try query_res.expect(.rows);
    \\    const rows_iter = rows.iter();
    \\    while (try rows_iter.next()) |row| {
    \\        var {%table_name_lower%}: {%table_name_lower%}_model.{%table_name_capital%} = undefined;
    \\        try row.scan(&{%table_name_lower%});
    \\        return {%table_name_lower%};
    \\    }
    \\
    \\    return {%table_name_lower%}_model.{%table_name_capital%}{};
    \\}
    \\
    \\pub fn update({%table_name_lower%}: *const {%table_name_lower%}_model.{%table_name_capital%}) !bool {
    \\    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    \\    const allocator = gpa.allocator();
    \\
    \\    var sql = String.init(allocator);
    \\    defer sql.deinit();
    \\    try sql.concat("UPDATE {%table_name_lower%} SET ");
    \\    try concat_cols_push(&sql, "", {%table_name_lower%}, ",");
    \\    try sql.concat(" WHERE {%pk_name%} = ?");
    \\
    \\    const client = db.get_db_client();
    \\    const prep_res = try client.prepare(allocator, sql.str());
    \\    defer prep_res.deinit(allocator);
    \\    const prep_stmt = try prep_res.expect(.stmt);
    \\    _ = try client.execute(&prep_stmt, .{{%table_name_lower%}.{%pk_name%}.?});
    \\
    \\    return true;
    \\}
    \\
    \\pub fn delete({%pk_name%}: i32) !bool {
    \\    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    \\    const allocator = gpa.allocator();
    \\    const client = db.get_db_client();
    \\    const prep_res = try client.prepare(allocator, "DELETE FROM {%table_name_lower%} WHERE {%pk_name%} = ?");
    \\    defer prep_res.deinit(allocator);
    \\    const prep_stmt = try prep_res.expect(.stmt);
    \\    _ = try client.execute(&prep_stmt, .{{%pk_name%}});
    \\    return true;
    \\}
    \\
    \\fn concat_cols_push(buffer: *String, prefix: []const u8, param: *const {%table_name_lower%}_model.{%table_name_capital%}, conj: []const u8) !void {
    \\    var first = true;
    \\{%concat_items%}
    \\}
;

const CONTROLLER_TEMPLATE =
    \\const std = @import("std");
    \\const httpz = @import("httpz");
    \\const {%table_name_lower%}_service = @import("../services/{%table_name_lower%}_service.zig");
    \\const {%table_name_lower%}_model = @import("../models/{%table_name_lower%}_model.zig");
    \\
    \\fn add(request: *httpz.Request, response: *httpz.Response) !void {
    \\    const {%table_name_lower%} = request.json({%table_name_lower%}_model.{%table_name_capital%}) catch {
    \\        try response.json(.{ .result = false }, .{});
    \\        return;
    \\    };
    \\
    \\    if ({%table_name_lower%} == null) {
    \\        try response.json(.{ .result = false }, .{});
    \\        return;
    \\    }
    \\
    \\    const res = try {%table_name_lower%}_service.add(&{%table_name_lower%}.?);
    \\    try response.json(.{ .result = res }, .{});
    \\}
    \\
    \\fn query(request: *httpz.Request, response: *httpz.Response) !void {
    \\    const {%table_name_lower%} = request.json({%table_name_lower%}_model.{%table_name_capital%}) catch {
    \\        try response.json(.{ .result = false }, .{});
    \\        return;
    \\    };
    \\
    \\    if ({%table_name_lower%} == null) {
    \\        try response.json(.{ .result = false }, .{});
    \\        return;
    \\    }
    \\
    \\    try {%table_name_lower%}_service.query(&{%table_name_lower%}.?, response);
    \\}
    \\
    \\fn query_by_pk(request: *httpz.Request, response: *httpz.Response) !void {
    \\    const form_data = try request.formData();
    \\    const id_s = form_data.get("{%pk_name%}");
    \\    if (id_s == null) {
    \\        try response.json(.{ .result = false }, .{});
    \\        return;
    \\    }
    \\    const {%pk_name%} = try std.fmt.parseInt(i32, id_s.?, 10);
    \\    const {%table_name_lower%} = try {%table_name_lower%}_service.query_by_pk({%pk_name%});
    \\    try response.json(.{ .result = {%table_name_lower%} }, .{});
    \\}
    \\
    \\fn update(request: *httpz.Request, response: *httpz.Response) !void {
    \\    const {%table_name_lower%} = request.json({%table_name_lower%}_model.{%table_name_capital%}) catch {
    \\        try response.json(.{ .result = false }, .{});
    \\        return;
    \\    };
    \\
    \\    if ({%table_name_lower%} == null) {
    \\        try response.json(.{ .result = false }, .{});
    \\        return;
    \\    }
    \\
    \\    const res = try {%table_name_lower%}_service.update(&{%table_name_lower%}.?);
    \\    try response.json(.{ .result = res }, .{});
    \\}
    \\
    \\fn delete(request: *httpz.Request, response: *httpz.Response) !void {
    \\    const form_data = try request.formData();
    \\    const id_s = form_data.get("{%pk_name%}");
    \\    if (id_s == null) {
    \\        try response.json(.{ .result = false }, .{});
    \\        return;
    \\    }
    \\    const {%pk_name%} = try std.fmt.parseInt(i32, id_s.?, 10);
    \\    const res = try {%table_name_lower%}_service.delete({%pk_name%});
    \\    try response.json(.{ .result = res }, .{});
    \\}
    \\
    \\pub fn route(router: anytype) void {
    \\    router.post("/{%table_name_lower%}/add", add, .{});
    \\    router.post("/{%table_name_lower%}/query", query, .{});
    \\    router.post("/{%table_name_lower%}/query_by_pk", query_by_pk, .{});
    \\    router.post("/{%table_name_lower%}/update", update, .{});
    \\    router.post("/{%table_name_lower%}/delete", delete, .{});
    \\}
    \\
;

const CONCAT_FUNC_ITEM =
    \\    if (param.{%field_name%} != null) {
    \\        if (!first) {
    \\            try buffer.concat(conj);
    \\        } else {
    \\            try buffer.concat(prefix);
    \\        }
    \\        first = false;
    \\        try buffer.concat("{%field_name%} = '");
    \\        try buffer.concat(param.{%field_name%}.?);
    \\        try buffer.concat("'");
    \\    }
    \\
;
